Who Owns Who? - A Quick Guide to Rust Ownership
In this tutorial, we will explore the concept of ownership in Rust, a unique feature that sets it apart from other programming languages. We'll learn about the rules of ownership, borrowing, and slices to ensure memory safety without a garbage collector. By understanding ownership in Rust, you'll be able to write programs that are highly efficient and safe from null or dangling pointers, common issues in other languages. This tutorial aims to give you a solid foundation on which to build your Rust programming skills.
Helpful prior knowledge
- Low-level concepts such as
- Memory Management
- Memory safety
- The Stack and the Heap
Learning Outcomes
- Understand how ownership in Rust works, which is a key feature of the language.
- Learn about borrowing and how it helps in accessing data in a safe manner.
- Learn to avoid common errors related to null or dangling pointers by leveraging Rust's ownership system.
Tutorial Steps
Total steps: 5
-
Step 1: Ownership and the Borrow-Checker Part 1
Rust uses the ownership system to avoid memory errors such as memory leaks, race conditions and double free. It also gains some benefits like not needing a garbage collector. Why follow this approach?
Whenever we manipulate data, we have to consider static allocations and dynamic allocations.
Statically allocating data is cheap. We know its size. We also expect it does not change throughout the entirety of the program. That’s why it’s stored entirely on the stack.
Dynamic allocations are tricky. Allocations can happen any time in the life of the program e.g. before the program ends, a variable’s data is dropped or overwritten. Its size is unknown, that's why it’s stored on the heap.
If done incorrectly, like two variables pointing to the same address of the heap data and then dropped together, or if not freed correctly by the other, this may cause memory-safety issues that may leak data or cause data corruption.
To prevent this from happening, Rust introduces the ownership system. Within the lifetime of the program, variables are tagged that they own data from the heap. This disallows accidental manipulation of data from non-owners of the data. Consider the code example below
Works fine right? This is because integer types have a known size and are statically allocated so they are just copied and pushed into the stack instead.
Now try to compile the code below.
This is because a
Stringis a dynamically allocated type. We don’t know when it will be changed throughout the program e.g. user input name varies in length. So we need to strictly guarantee to the compiler that no other parts of the program, e.g. another variable, is able to change it except the variable that owns it. Hence, the variablexowns theStringdata and the only one that can do anything to the data unless we change ownership.This is the error message we get when we try to compile the erroneous code
Output of a failed cargo build on the right split. What actually happened is the data owned by variable
xis now owned by variableyand thus, the ownership is moved andxis dropped andxno longer exists. So trying to access a non-existent variable is erroneous. The original data is now owned by the variabley.A simple illustration of a move. Variable x ownership on the data is moved to y. x will not exist as it is dropped after the move. We can actually follow what the compiler suggests, that is to clone the data. Cloning copies the original data from variable x on the heap and writes that exact copy on another region of the heap. That assigned variable is now the owner of that data.
A simple illustration of how a clone works. Data from the heap is cloned for y to own. See the changed code below
Output of a successful cargo build and cargo run. But a clone is not a cheap operation. It allocates a new region in memory and wastes precious resources. To solve this problem, we can tag a reference of the data instead of its actual value.
We can then borrow this data without transferring ownership and without cloning that causes unnecessary allocations!
To do that, when we assign
xtoy, we use a reference annotation symbol, ampersand, to variablex, which now looks like&x.A simple illustration of y borrowing the address (a reference) of x Below is the change code
and below is the image of a successful compilation.
Output of a successful cargo build and cargo run. Alright, that was a lot to take in. So for now you might want to take a break before we can resume about this topic.
-
Step 2: Ownership and the Borrow-Checker Part 2
Now that you are here, let's resume understanding the ownership system in Rust.
The last thing we talked about is how to create a reference of a value. A reference is just an address of where the data is located on the heap. This is useful for dynamically allocating data since we don’t want other parts of the program to take ownership of it or have multiple owners which is impossible in Rust.
By default, borrowing a reference is immutable. Since an immutable reference has a known size and we are guaranteeing the compiler that to be immutable, we can have multiple immutable references as shown below
We can only make a mutable reference from a mutable variable. Check out the code below
This means a mutable reference can actually let us change the data. This also means we cannot have multiple references of the data assigned after to other variables because it violates the idea of avoiding a race condition.
A race condition is when two or more variables, operations or functions are changing the same data in the heap at nearly the same time.The following code snippets below shows us how it works.
At first glance, it looks fine. We can actually compile the code as shown by the image below
We can change x with no issues because it’s the owner of the data 🤔 You can see the image below
However, trying to borrow and change a mutable reference will throw an error like the code below
Trying to compile this code will give us a compile error. Let's learn about this in the next step!
-
Step 3: Ownership and the Borrow-Checker Part 3
The last code snippet from the previous step will give us this compiler error message
Order matters here. If we replace
with
it will compile just fine. The previous code errors because we are trying to access the reference in variable
ywhen there is already a mutable reference after it and also within function scope. This is shown better if we try to access both variables as shown belowBased on our previous example, order matters. The last that was assigned the mutable reference should be the only one to use that reference.
If we already have multiple assigned immutable references, it works fine. However, we cannot assign both mutable and immutable references at the same time as shown by the code below
and thus, giving us a thrown error below
-
Step 4: Ownership and Functions
Functions also follow the rules of ownership but it only happens within functions. Functions are very specific of what parameter types and return types they have. Check out the code below.
This code will throw an error as shown by the image below because we are moving ownership into the function
You can fix that by cloning variable
sas shown by the code belowWe can change both the parameter type and the return type as references shown by the code below
or clone it within the function
Try to code in such a way that we avoid unnecessary allocations. We might want a function that gives a variable ownership of the data or a reference of the data.
We might want to have one owner of the data while changing it within a function as well. To do that, we just add the
&mutto the parameter and we actually do not need to have the return type as shown by the code belowThis code will have an output as shown by the image below
⚠️ Please do note that functions pass around ownership and references and unlike variables, they cannot assign themselves to own or to have a reference of a data. But they can move ownership.
Things like the code below can be confusing at first since we have two or more
&mutAnd the code compiles as shown by the image below
💡 The key here is to look at the return type. We didn’t return anything. So the code before will compile. However, if we do add a return AND assign the returned references to a variable like the code below
we are actually bringing these references back into the function scope of main and thus, the borrow checker sees that it violated some rules now and will throw an error.
-
Step 5: Conclusion
We finally have finished Rust Basics. Although there might be new content or revisions in the future, this module should be enough to give you a quick start to Rust development. So what's next?
Here are things that you should do:
- Start reading.
- Learn how to read documentation. Don't underestimate this skill!
- Experiment concepts. You understand more if you see the process.
- Write your own projects. It does not have to be great as long as you can learn.
- Build communities and connections. The more people that are learning with you, the merrier.
- Be kind. Do not discriminate. Learn to be kind to other learners.
- Pace yourself. Do not be hasty or you risking yourself of burnout.
- Rinse and repeat.
Find articles to support you through your journey or chat with our support team.
Help Center